/*
 *  Copyright 2008 The MITRE Corporation (http://www.mitre.org/). All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */


package org.mitre.lattice.query;

import org.mitre.mrald.util.MetaData;import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;

import org.mitre.mrald.control.AbstractStep;
import org.mitre.mrald.control.MsgObject;
import org.mitre.mrald.control.WorkflowStepException;
import org.mitre.mrald.ddlelements.DdlElementComparator;
import org.mitre.mrald.ddlelements.InsertElement;
import org.mitre.mrald.ddlelements.MultiInsertElementComparator;
import org.mitre.mrald.parser.ParserElement;
import org.mitre.mrald.util.Config;
import org.mitre.mrald.util.FormTags;
import org.mitre.mrald.util.MiscUtils;
import org.mitre.mrald.util.MraldConnection;
import org.mitre.mrald.util.MraldException;
import org.mitre.mrald.util.MraldOutFile;
import org.mitre.mrald.util.User;
/**
 *  Updates any newly inserted objects to have the correct label Creates new
 *  Update Builder Elements, based on the whether the inserted elements are also
 *  labelled in the database. Will parse through elements, and find the insert
 *  elements If table name is in the list of labelled table, then creates a new
 *  UpdateQuery, with same id, labelTable, and check that owner, is the same as
 *  the User logged in.
 *
 *@author     ghamilton
 *@created    July 23rd, 2004
 */
public class UpdateLabel extends AbstractStep
{
	MsgObject msg;


	/**
	 *  Method will take the
	 *
	 *@param  msgObject                  received from the previous step - contains
	 *      all information to be processed
	 *@exception  WorkflowStepException  Description of the Exception
	 */
	public void execute(MsgObject msgObject)
		 throws WorkflowStepException
	{
        if ( !Config.getLatticeFactory().getUsingLatticeSecurityModel() )
        {
            return;
        }
		try
		{
			msg = msgObject;
			User user = (User) msg.getReq().getSession().getAttribute(Config.getProperty("cookietag"));

			/*
			 *  extract the InsertElements from the parser elements and sort them by the DdlComparator
			 */
			ArrayList<ParserElement> parserElements = msgObject.getWorkingObjects();
			List<InsertElement> c = extractInsertElements(parserElements);
			removeInvalidElements( c );

			boolean isMulti = hasMultipleThreads(c);

			if (isMulti) Collections.sort(c, new MultiInsertElementComparator());
			else
			    Collections.sort(c, new DdlElementComparator());
			/*
			 *  build the DDL statements (one for each table listed in the InsertElemetns)
			 *  and store them in the MsgObject query field for the next workflow step to use
			 */
			String ddl[] = buildUpdates(c, user);

			//set query appends to the end of the last array
			for (int i = 0; i < ddl.length; i++)
			{
				msgObject.setQuery(ddl[i]);
				//MraldOutFile.logToFile(Config.getProperty("LOGFILE"), ddl[i]);
			}

		} catch (MraldException e)
		{
			throw new WorkflowStepException(e);
		}
	}


	/**
	 *  Build the DDL statements (one for each table listed in the InsertElements)
	 *  and store them in the MsgObject query field for the next workflow step to
	 *  use
	 *
	 *@param  insertElements      Description of the Parameter
	 *@param  user                Description of the Parameter
	 *@return                     Description of the Return Value
	 *@exception  MraldException  Description of the Exception
	 */
	public String[] buildUpdates(List insertElements, User user)
		 throws MraldException
	{
		boolean tableLabelled = false;
		boolean finishThread = false;
		/*
		 *  set up return ArrayList
		 */
		ArrayList<InsertElement> commonElements = new ArrayList<InsertElement>();
		ArrayList<InsertElement> multiElements = new ArrayList<InsertElement>();
		/*
		 *  split up by table
		 */
		ArrayList<InsertElement> elements = new ArrayList<InsertElement>();
		ArrayList<String> updateSqls = new ArrayList<String>();

		InsertElement currentElement = null;
//		int sqlThreadNo = 0;

		String tableName = "";

		HashMap tableList = getLabelTables();

		for (int i = 0; i < insertElements.size(); i++)
		{
			currentElement = (InsertElement) insertElements.get(i);


			tableName = currentElement.getTable();
			tableLabelled = tableLabelled(tableName, tableList);

			String threadNo = currentElement.getSqlThread();
			if (threadNo == "")
			{
				threadNo = "1";
			}

			//if this is the last element then finish processing
			if (i == (insertElements.size() - 1))
			{
				finishThread = true;
			}
			else
			{
			        finishThread = finishThread(threadNo, (InsertElement) insertElements.get(i + 1));
			}
			//If this current table is not labelled
			if (!tableLabelled)
			{
				//reset
				elements = new ArrayList<InsertElement>();
				continue;
			}


			/*
			 *  grab all the elements that should be in more than one query
			 */
			if (currentElement.getSqlThread().indexOf(",") != -1)
			{
				multiElements.add(currentElement);
			}
			/*
			 *  grab all the common elements.  This only works because the elements
			 *  are already sorted.  All negative values should be at the front of the list,
			 *  so we use a negative SqlThread number to indicate a "common" part of a query
			 */
			else if (Integer.parseInt(threadNo) < 0)
			{
				commonElements.add(currentElement);
			}
			else //(!elements.contains(currentElement))
			{
				elements.add(currentElement);
			}

			if (finishThread)
			{

				if (tableLabelled)
				{

					//MraldOutFile.logToFile( Config.getProperty( "LOGFILE" ), "Update Label: Finish Threads: TableName : " + currentElement.getTable() );

					/*
					 *  add the elements that need to be in more than one query
					 */
					//MraldOutFile.logToFile( Config.getProperty( "LOGFILE" ), "Update Label: Finish Threads: No of Common elements : " + commonElements.size() );
					//MraldOutFile.logToFile( Config.getProperty( "LOGFILE" ), "Update Label: Finish Threads: No of Multi elements : " + multiElements.size() );

					//printElements( commonElements, "commonElements");
					//printElements( multiElements, "multiElements");

					elements.addAll(commonElements);
					elements = addToSqlThread(multiElements, elements, threadNo);

					//printElements( elements, "ALLElements");

					updateSqls.add(updateLabels(elements, tableList, user));

					//elements is now the total list of elements for this thread

				}
				elements = new ArrayList<InsertElement>();

			}
		}

		String[] returnList = new String[updateSqls.size()];

		returnList = updateSqls.toArray(returnList);
		return returnList;
	}


	/**
        *  Adds a feature to the ToSqlThread attribute of the MultiInsertBuilder
	 *  object
	 *
	 *@param  commonElements  The feature to be added to the ToSqlThread attribute
	 *@param  ddlElements     The feature to be added to the ToSqlThread attribute
	 *@param  threadNo        The feature to be added to the ToSqlThread attribute
	 */
	protected void printElements(ArrayList elems, String label)
	{

		for (int i=0; i <elems.size(); i++)
		{
			MraldOutFile.logToFile( Config.getProperty( "LOGFILE" ), "Update Label: print elements: " + elems.get(i).toString() );
		}
	}

	/**
	 *  Adds a feature to the ToSqlThread attribute of the MultiInsertBuilder
	 *  object
	 *
	 *@param  commonElements  The feature to be added to the ToSqlThread attribute
	 *@param  ddlElements     The feature to be added to the ToSqlThread attribute
	 *@param  threadNo        The feature to be added to the ToSqlThread attribute
	 */
	protected ArrayList<InsertElement> addToSqlThread(ArrayList<InsertElement> commonElements, ArrayList<InsertElement> ddlElements, String threadNo)
	{
		InsertElement e;
		//MraldOutFile.logToFile( Config.getProperty( "LOGFILE" ), "Update Label: addToSqlThread: Start "  );

		for (int i = 0; i < commonElements.size(); i++)
		{

			e = commonElements.get(i);
			//MraldOutFile.logToFile( Config.getProperty( "LOGFILE" ), "Update Label: addToSqlThread: Element " +   e.toString() );
			String[] nums = e.getSqlThread().split(",");
			//MraldOutFile.logToFile( Config.getProperty( "LOGFILE" ), "Update Label: addToSqlThread: NUms " +   nums.toString() );
			for (int j = 0; j < nums.length; j++)
			{
				//MraldOutFile.logToFile( Config.getProperty( "LOGFILE" ), "Update Label: addToSqlThread: Num " +   nums[j] +  " being compared to " + threadNo );

				if (nums[j].equals(threadNo))
				{
					//MraldOutFile.logToFile( Config.getProperty( "LOGFILE" ), "Update Label: addToSqlThread: Num " +   nums[j] +  " being added to " + threadNo );

					ddlElements.add(e);
				}
			}
		}
		return ddlElements;
	}


	/*
	 *  Removes the InsertElements from the supplied ArrayList and returns them
	 *  in a separate Collection object.
	 *
	 *  @param  parserElements  Description of the Parameter
	 *  @return                 Description of the Return Value
	 */
	/**
	 *  Description of the Method
	 *
	 *@param  parserElements  Description of the Parameter
	 *@return                 Description of the Return Value
	 */
	private List<InsertElement> extractInsertElements(ArrayList<ParserElement> parserElements)
	{
		ArrayList<InsertElement> returnList = new ArrayList<InsertElement>();
		ParserElement nextElement;
		for (int i = 0; i < parserElements.size(); i++)
		{
			nextElement = parserElements.get(i);
			if (nextElement instanceof InsertElement)
			{
				/*
				 *  add it to the return ArrayList
				 */
				returnList.add((InsertElement)parserElements.get(i));
			}
		}
		/*
		 *  Remove all the elements in the returnList from the passed ArrayList
		 */
		parserElements.removeAll(returnList);
		return returnList;
	}


	/*
	 *  Removes the InsertElements from the supplied ArrayList and returns them
	 *  in a separate Collection object.
	 *
	 *  @param  parserElements  Description of the Parameter
	 *  @return                 Description of the Return Value
	 */
	/**
	 *  Gets the labelTables attribute of the UpdateLabel object
	 *
	 *@return                     The labelTables value
	 *@exception  MraldException  Description of the Exception
	 */
	private HashMap<String,String> getLabelTables() throws MraldException
	{
		try
		{
			MraldConnection conn = new MraldConnection(MetaData.ADMIN_DB);

			String selectStr = "select * from label_sql ";
			ResultSet rs = conn.executeQuery(selectStr);

			HashMap<String,String> labelTables = new HashMap<String,String>();
			while (rs.next())
			{
				labelTables.put(rs.getString("table_name").toUpperCase(), rs.getString("sql_clause"));

			}
			rs.close();
			conn.close();

			return labelTables;
		} catch (SQLException e)
		{
			throw new MraldException(e.getMessage());
		}
	}


	/*
	 *  Removes the InsertElements from the supplied ArrayList and returns them
	 *  in a separate Collection object.
	 *
	 *  @param  parserElements  Description of the Parameter
	 *  @return                 Description of the Return Value
	 */
	/**
	 *  Description of the Method
	 *
	 *@param  table        Description of the Parameter
	 *@param  labelTables  Description of the Parameter
	 *@return              Description of the Return Value
	 */
	private boolean tableLabelled(String table, HashMap labelTables)
	{

		if (table.equals(FormTags.INSERT_INTO_ALL_TABLES))
		{
			return true;
		}
                table = table.toUpperCase();

		if ( !labelTables.containsKey( table )) return false;

		String query = labelTables.get(table).toString().toUpperCase();


		String tableShared = table.toUpperCase() + ".";

		if ( query.indexOf( tableShared ) > -1 ) return true;
		else return false;
	}


	/*
	 *  Removes the InsertElements from the supplied ArrayList and returns them
	 *  in a separate Collection object.
	 *
	 *  @param  parserElements  Description of the Parameter
	 *  @return                 Description of the Return Value
	 */
	/**
	 *  Description of the Method
	 *
	 *@param  elems               Description of the Parameter
	 *@param  tableList           Description of the Parameter
	 *@param  user                Description of the Parameter
	 *@return                     Description of the Return Value
	 *@exception  MraldException  Description of the Exception
	 */
	private final String updateLabels(ArrayList elems, HashMap tableList, User user) throws MraldException
	{

		//This is the list of arrayElements that need to be used to update the table.
		String tableName = null;

		for (int i=0; i < elems.size(); i++)
		{
		    tableName  = ((InsertElement) elems.get(i)).getTable().toUpperCase();
		    if (!tableName.equals(FormTags.INSERT_INTO_ALL_TABLES.toUpperCase())) break;
		}

		String sqlClause = tableList.get(tableName).toString();
		String groupName = user.getGroup();
		//MraldOutFile.logToFile( Config.getProperty( "LOGFILE" ), "Update Label: update Labels:  Label " + groupName);
		groupName = MiscUtils.checkApostrophe(groupName );

		String buildSql = "Update <:tableName:>_label set label = '" + groupName + "', owner = '<:userName:>'  where ";

		//Get list of keys, to use to update the label
		//Subject.subject_id = Subject_Label.subject_id  AND Subject_Label.Label IN
		InsertElement element = null;
		ArrayList<String> fieldStrings = new ArrayList<String>();
		ArrayList<String> valueStrings = new ArrayList<String>();

		for (int i = 0; i < elems.size(); i++)
		{
			element = (InsertElement) elems.get(i);
			fieldStrings = element.buildFieldList(fieldStrings);
			valueStrings = element.buildValueList(valueStrings);

		}

		for (int i = 0; i < fieldStrings.size(); i++)
		{
			String field = fieldStrings.get(i).toString().toUpperCase();

			//IF the field is contained within the label_sql statement , then get value
			//and use to build update statement
			if (sqlClause.toUpperCase().indexOf(field) > 0)
			{
				buildSql = buildSql + fieldStrings.get(i) + " = " + valueStrings.get(i) + " AND ";

			}
		}
		//strip off the last " AND "
		buildSql = buildSql.substring(0, buildSql.length() - 5);

		//Get Id of the owner, in order to update the label table with owner name

		//MraldOutFile.logToFile( Config.getProperty( "LOGFILE" ), "Update Label: update Labels:  Label after " + groupName);



		buildSql = buildSql.replaceAll("<:userName:>", user.getEmail());

		buildSql = buildSql.replaceAll("<:tableName:>", tableName);
		//Get list of Values, for each of the key names


		return buildSql;
	}


	/**
	 *  Method to cycle through elements to detect if there are muliple threads
	 *
	 *@param  threadNo            Description of the Parameter
	 *@param  nextElements        Description of the Parameter
	 *@return                     true: hasMultiple threads.
	 *@exception  MraldException  Description of the Exception
	 */
	private boolean finishThread(String threadNo, InsertElement nextElements)
		 throws MraldException
	{
		//If this is a thread to be applied to all

		if ( threadNo.indexOf( "," ) > 0 )
		{

		    String[] nums = threadNo.split( "," );
		    //MraldOutFile.logToFile( Config.getProperty( "LOGFILE" ), "Update Label: Finish Threads: split : " + nums[0] );
		    //Element is grouped with the lowest number in the string
		    threadNo = nums[0];
		}
		//MraldOutFile.logToFile( Config.getProperty( "LOGFILE" ), "Update Label: Finish Threads: thread Number : " + threadNo );

		if (Integer.parseInt(threadNo) ==  -1) return false;

		String newThreadNo = nextElements.getSqlThread();

		if (newThreadNo == "")
		{
			newThreadNo = "1";
		}
		else if ( newThreadNo.indexOf( "," ) > 0 )
		{

		    String[] nums = newThreadNo.split( "," );

		    newThreadNo = nums[0];
		}

		return !newThreadNo.equals(threadNo);
	}


	/**
	 *  Method to cycle through elements to detect if there are muliple threads
	 *
	 *@param  insertElements      Collection of any InsertElemenst
	 *@return                     true: hasMultiple threads.
	 *@exception  MraldException  Description of the Exception
	 */
	private boolean hasMultipleThreads(List insertElements)
		 throws MraldException
	{
		String threadNo = "";

		if (insertElements.size() == 0)
		{
			return false;
		}

		String prevThreadNo = ((InsertElement) insertElements.get(0)).getSqlThread();

		for (int i = 0; i < insertElements.size(); i++)
		{
			InsertElement currentElement = (InsertElement) insertElements.get(i);

			threadNo = currentElement.getSqlThread();
			//as soon as a new thread s detected then flag that there is multiple threads
			if (threadNo != prevThreadNo)
			{
				return true;
			}

			prevThreadNo = threadNo;
		}
		return false;
	}

	/**
     *  Removes all invalid InsertElements from the passed Collection. An
     *  invalid InsertElement is one with no Value provided.
     *
     *@param  c                   A Collection of InsertElements
     *@exception  MraldException  Description of the Exception
     */
    public void removeInvalidElements( Collection c )
        throws MraldException
    {

        Iterator iter = c.iterator();
        InsertElement currentElement;
        ArrayList<String> valueStrings = new ArrayList<String>();
        int lastSize = 0;
        while ( iter.hasNext() )
        {
            currentElement = ( InsertElement ) iter.next();
            valueStrings = currentElement.buildValueList( valueStrings );
            if ( valueStrings.size() == lastSize )
            {
                iter.remove();
                lastSize--;
            }
            lastSize++;
        }
    }
}

